﻿using System;
using System.Configuration;
using System.Xml;

namespace Fadd.Logging
{
	/// <summary>
	/// Reads logging configuration from <c>app.config</c> or <c>web.config</c>.
	/// </summary>
	/// <remarks>
	/// 
	/// </remarks>
	/// <example>
	/// <code>
	/// &lt;?xml version="1.0" encoding="utf-8" ?&gt;
	/// &lt;configuration&gt;
	///   &lt;configSections&gt;
	///     &lt;section name="Logging"
	///             type="Fadd.Logging.ConfigSectionHandler, Fadd.Logging"/&gt;
	///   &lt;/configSections&gt;
	/// 
	///   &lt;Logging&gt;
	///     &lt;Targets&gt;
	///       &lt;Target Type="ConsoleTarget" Name="console" Formatter="ConsoleFormatter"&gt;
	///         &lt;WarningColor&gt;Red&lt;/WarningColor&gt;
	///       &lt;/Target&gt;
	///       &lt;Target Type="FileTarget" Name="fileWithRotation" Formatter="DefaultFormatter"&gt;
	///         &lt;DaysToKeep&gt;7&lt;/DaysToKeep&gt;
	///       &lt;/Target&gt;
	///     &lt;/Targets&gt;
	///     &lt;Loggers&gt;
	///       &lt;Logger Name="Users" Namespace="MyProgram.User.*" Targets="console, fileWithRotation" MinLevel="Debug" /&gt;
	///       &lt;Logger Name="Logins" Namespace="MyProgram.User.LoginHandler" Targets="console" /&gt;
	///     &lt;/Loggers&gt;
	///     &lt;Formatters&gt;
	///       &lt;Formatter Name="ConsoleFormatter" Type="DefaultFormatter" Format="{Date:yyyy-mm-dd hh:mm:ss.ffff} {ThreadId} {StackTrace:3,40} {Message}" /&gt;
	///       &lt;Formatter Name="MyCustomFormatter" Type="MyProgram.Logging.CustomFormatter, MyProgram" /&gt;
	///     &lt;/Formatters&gt;
	///   &lt;/Logging&gt;
	/// &lt;/configuration&gt;
	/// </code>
	/// </example>
	public class ConfigSectionHandler : IConfigurationSectionHandler
	{
		private readonly Configuration _config = new Configuration();

		#region IConfigurationSectionHandler Members

		/// <summary>Creates a configuration section handler.</summary>
		/// <param name="parent">Parent object.</param>
		/// <param name="configContext">Configuration context object.</param>
		/// <param name="section">Section XML node.</param>
		/// <returns>The created section handler object.</returns>
		/// <exception cref="ConfigurationErrorsException">If configuration is incorrect.</exception>
		public object Create(object parent, object configContext, XmlNode section)
		{
			foreach (XmlNode node in section.ChildNodes)
			{
				switch (node.Name)
				{
					case "Targets":
						ParseTargets(node.ChildNodes);
						break;
					case "Loggers":
						ParseLoggers(node.ChildNodes);
						break;
					case "Formatters":
						ParseFormatters(node.ChildNodes);
						break;
				}
			}

			return _config;
		}

		#endregion

		/// <summary>
		/// Get an attribute value from a node.
		/// </summary>
		/// <param name="node">Node to get the attribute from</param>
		/// <param name="configSection">Configuration section that the attribute is in.</param>
		/// <param name="attribute">Name of attribute</param>
		/// <param name="isMandatory"><c>true</c> if attribute is mandatory; otherwise false.</param>
		/// <returns>attribute value</returns>
		/// <exception cref="ConfigurationErrorsException">Attribute is not specified but mandatory.</exception>
		public static string GetAttribute(XmlNode node, string configSection, string attribute, bool isMandatory)
		{
			if (node.Attributes == null)
				throw new ConfigurationErrorsException("Attribute '" + attribute +
													   "' is mandatory for " + configSection + " configuration.");
			XmlAttribute aattribute = node.Attributes[attribute];
			if (aattribute == null && isMandatory)
				throw new ConfigurationErrorsException("Attribute '" + attribute +
				                                       "' is mandatory for " + configSection + " configuration.");
			return aattribute == null ? null : aattribute.InnerText;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="nodes"></param>
		/// <example>
		/// <code>
		///     &lt;Formatters&gt;
		///       &lt;Formatter Name="ConsoleFormatter" Type="DefaultFormatter" Format="{Date:yyyy-mm-dd hh:mm:ss.ffff} {ThreadId} {StackTrace:3,40} {Message}" /&gt;
		///       &lt;Formatter Name="MyCustomFormatter" Type="MyProgram.Logging.CustomFormatter, MyProgram" /&gt;
		///     &lt;/Formatters&gt;
		/// </code>
		/// </example>
		/// <exception cref="ConfigurationErrorsException"><c>ConfigurationErrorsException</c>.</exception>
		private void ParseFormatters(XmlNodeList nodes)
		{
			foreach (XmlNode node in nodes)
			{
				if (node is XmlComment)
					continue;

				var config = new FormatterConfiguration
				             	{
				             		Name = GetAttribute(node, "Formatters", "Name", true),
									Format = GetAttribute(node, "Formatters", "Format", false)
				             	};

				string typeName = GetAttribute(node, "Formatters", "Type", true);
				if (!typeName.Contains("."))
					typeName = "Fadd.Logging." + typeName;

				config.Type = Type.GetType(typeName, false);
				if (config.Type == null)
					throw new ConfigurationErrorsException("Could not find formatter type '" + typeName + "'", node);

				config.ChildConfiguration = node.ChildNodes;
				_config.Formatters.Add(config);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="nodes"></param>
		/// <example>
		/// <code>
		///     &lt;Loggers&gt;
		///       &lt;Log Name="Users" Namespace="MyProgram.User.*" Targets="console, fileWithRotation" MinLevel="Debug" /&gt;
		///       &lt;Log Name="Logins" Namespace="MyProgram.User.LoginHandler" Targets="console" /&gt;
		///     &lt;/Logger&gt;
		/// </code>
		/// </example>
		/// <exception cref="ConfigurationErrorsException"><c>ConfigurationErrorsException</c>.</exception>
		private void ParseLoggers(XmlNodeList nodes)
		{
			foreach (XmlNode node in nodes)
			{
				if (node is XmlComment)
					continue;
				var configuration = new LoggerConfiguration
				                    	{
				                    		ChildConfiguration = node.ChildNodes,
											Namespace = GetAttribute(node, "Loggers", "Namespace", true),
											Name = GetAttribute(node, "Loggers", "Name", true)
				                    	};

				// load targets
				string[] names = GetAttribute(node, "Loggers", "Targets", true).Split(',');
				foreach (string name in names)
					configuration.TargetNames.Add(name.Trim());

				// get minimum debug level.
				try
				{
					string minLevel = GetAttribute(node, "Loggers", "MinLevel", false);
					if (!string.IsNullOrEmpty(minLevel))
						configuration.MinLevel = (LogLevel) Enum.Parse(typeof (LogLevel), minLevel);
					else
						configuration.MinLevel = LogLevel.Trace;
				}
				catch (Exception err)
				{
					throw new ConfigurationErrorsException(
						"Failed to parse minimum log level for " + configuration.Name + ".", err, node);
				}

				_config.Loggers.Add(configuration);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="nodes"></param>
		/// <example>
		/// <code>
		///     &lt;Targets&gt;
		///       &lt;Target Type="ConsoleTarget" Name="console" Formatter="ConsoleFormatter"&gt;
		///         &lt;Color LogLevel="Warning"&gt;ConsoleColor.Red&lt;/Color&gt;
		///       &lt;/Target&gt;
		///       &lt;Target Type="FileTarget" Name="fileWithRotation" Formatter="DefaultFormatter"&gt;
		///         &lt;DaysToKeep&gt;7&lt;/DaysToKeep&gt;
		///       &lt;/Target&gt;
		///     &lt;/Targets&gt;
		/// </code>
		/// </example>
		/// <exception cref="ConfigurationErrorsException">Failed to load target type</exception>
		private void ParseTargets(XmlNodeList nodes)
		{
			foreach (XmlNode node in nodes)
			{
				if (node is XmlComment)
					continue;
				var config = new TargetConfiguration();
				config.Name = GetAttribute(node, "Targets", "Name", true);
				config.ChildConfiguration = node.ChildNodes;

				// fix formatter.
				config.FormatterName = GetAttribute(node, "Targets", "Formatter", false);
				if (string.IsNullOrEmpty(config.FormatterName))
					config.FormatterName = "DefaultFormatter";

				// Load target type
				string typeName = GetAttribute(node, "Targets", "Type", true);
				if (!typeName.Contains("."))
					typeName = "Fadd.Logging.Targets." + typeName;
				config.TargetType = Type.GetType(typeName, false);
				if (config.TargetType == null)
					throw new ConfigurationErrorsException("Failed to load target type '" + typeName + "'.", node);

				_config.Targets.Add(config);
			}
		}
	}
}